// -*- c++ -*-
//
// deliciousapi.cpp
//
// Copyright (C) 2005 Petter Stokke <ummo@hellokitty.com>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the
// Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
//

#include "deliciousapi.h"

#include <klocale.h>
#include <kdebug.h>
#include <kmessagebox.h>
#include <kio/authinfo.h>

#include <qtimer.h>
#include <qregexp.h>



#define ASSERT_XML_ELEMENT(el, name) if (el.tagName() != name) { \
    KMessageBox::error(0, i18n("Invalid XML document received from del.icio.us."), i18n("dialog title", "del.icio.us Sidebar")); \
    emit failed(this); return; }

#define ASSERT_XML_VALID(node) if (node.isNull()) { \
    KMessageBox::error(0, i18n("Invalid XML document received from del.icio.us."), i18n("dialog title", "del.icio.us Sidebar")); \
    emit failed(this); return; }



// --- DeliciousAPIJob ---

DeliciousAPIJob::DeliciousAPIJob( DeliciousAPI* api, const char* name )
        : BookmarkAPIJob( api, name )
        , m_api( api )
        , m_isDone( false )
        , m_isRunning( false )
{}

DeliciousAPI* DeliciousAPIJob::api() const
{
    return m_api;
}

const KURL& DeliciousAPIJob::apiURL() const
{
    return m_api->apiURL();
}

bool DeliciousAPIJob::isRunning() const
{
    return m_isRunning;
}

bool DeliciousAPIJob::isDone() const
{
    return m_isDone;
}

void DeliciousAPIJob::done()
{
    m_isDone = true;
}

void DeliciousAPIJob::startJob( const KURL& command )
{
    m_commandURL = command;
    m_isRunning = true;
    m_isDone = false;
    m_totalSize = m_processedSize = 0;
    kdDebug() << "Starting del.icio.us job \"" << command << "\"" << endl;
    KIO::StoredTransferJob* job = KIO::storedGet( command, true, false );
    connect( job, SIGNAL( result( KIO::Job* ) ), this, SLOT( jobResult( KIO::Job* ) ) );
    connect( job, SIGNAL( totalSize( KIO::Job*, KIO::filesize_t ) ), this, SLOT( slotTotalSize( KIO::Job*, KIO::filesize_t ) ) );
    connect( job, SIGNAL( processedSize( KIO::Job*, KIO::filesize_t ) ), this, SLOT( slotProcessedSize( KIO::Job*, KIO::filesize_t ) ) );
}

void DeliciousAPIJob::slotTotalSize( KIO::Job*, KIO::filesize_t size )
{
    m_totalSize = size;
    emit jobProgress( m_processedSize, m_totalSize );
}

void DeliciousAPIJob::slotProcessedSize( KIO::Job*, KIO::filesize_t size )
{
    m_processedSize = size;
    emit jobProgress( m_processedSize, m_totalSize );
}

void DeliciousAPIJob::jobResult( KIO::Job* j )
{
    emit jobDone();
    KIO::StoredTransferJob* job = dynamic_cast<KIO::StoredTransferJob*>( j );
    ASSERT( job );
    done();
    if ( job->error() ) {
        job->showErrorDialog( 0 );
        emit failed( this );
    } else {
        QString doc( QString::fromUtf8( job->data().data(), job->data().size() ) );
        if ( m_xml.setContent( doc, true ) ) {
            kdDebug() << "URL \"" << m_commandURL << "\" returned well-formed XML document:" << endl;
            QString xmldbg = m_xml.toString();
            if ( xmldbg.length() > 256 ) {
                xmldbg.truncate( 256 );
                xmldbg += " [...]";
            }
            kdDebug() << xmldbg << endl;
            processData();
        } else {
            KMessageBox::error( 0, i18n( "Malformed XML document received from del.icio.us." ), i18n( "dialog title", "del.icio.us Sidebar" ) );
            emit failed( this );
        }
    }
}

// --- RSSJob ---

RSSJob::RSSJob( DeliciousAPI* api, const char* name )
        : DeliciousAPIJob( api, name )
{}

void RSSJob::processData()
{
    RSSFeed feed;
    if ( !feed.parse( m_xml ) ) {
        KMessageBox::error( 0, i18n( "Malformed RSS feed received from del.icio.us." ), i18n( "dialog title", "del.icio.us Sidebar" ) );
        emit failed( this );
    }
    processFeed( feed );
}



// --- DeliciousAPI ---

DeliciousAPI::DeliciousAPI( QObject* parent, const char* name, const KURL& apiURL )
        : BookmarkAPI( parent, name )
        , m_apiURL( apiURL )
{
    if ( m_apiURL.isEmpty() )
        m_apiURL = KURL( "https://api.del.icio.us/v1" );
    m_jobs.setAutoDelete( true );
    m_timer = new QTimer( this );
    connect( m_timer, SIGNAL( timeout() ), this, SLOT( scheduler() ) );
    m_timer->start( 1200 ); // Schedule a new job every 1.2 seconds.
    findUser();
}

const KURL& DeliciousAPI::apiURL() const
{
    return m_apiURL;
}

void DeliciousAPI::findUser()
{
    // First, ask kpasswdserver for 401 auth data.
    QByteArray data, replyData;
    QCString replyType;
    QDataStream arg( data, IO_WriteOnly );
    KIO::AuthInfo info;
    info.url = apiURL().url();
    arg << info << ( long ) 0;
    if ( !m_dcop.call( "kded", "kpasswdserver", "checkAuthInfo(KIO::AuthInfo, long int)", data, replyType, replyData, true, -1 ) ) {
        kdDebug() << "Failed to perform DCOP call to kpasswdserver." << endl;
        return ;
    }
    if ( replyType != "KIO::AuthInfo" ) {
        kdDebug() << "DCOP call received \"" << replyType << " return value, expected KIO::AuthInfo." << endl;
        return ;
    }
    QDataStream rv( replyData, IO_ReadOnly );
    rv >> info;
    if ( !info.username.isEmpty() ) {
        setUser( info.username );
        return ;
    }

    // If kpasswdserver doesn't have anything, there ought to be a cookie in kcookiejar.
    QByteArray data2, replyData2;
    QDataStream arg2( data2, IO_WriteOnly );
    arg2 << apiURL().url();
    if ( !m_dcop.call( "kded", "kcookiejar", "findCookies(QString)", data2, replyType, replyData2, true, -1 ) ) {
        kdDebug() << "Failed to perform DCOP call to kcookiejar." << endl;
        return ;
    }
    if ( replyType != "QString" ) {
        kdDebug() << "DCOP call received \"" << replyType << " return value, expected QString." << endl;
        return ;
    }
    QDataStream rv2( replyData2, IO_ReadOnly );
    QString cookie;
    rv2 >> cookie;
    QRegExp re( "user=([^;%]+)" );
    if ( re.search( cookie ) < 0 ) {
        kdDebug() << "Cookie string \"" << cookie << "\" contains no user data I can find." << endl;
        return ;
    }
    setUser( QStringList::split( ";", re.cap( 1 ) ) [ 0 ] );
}

void DeliciousAPI::scheduleJob( DeliciousAPIJob* job )
{
    m_jobs.append( job );
}

void DeliciousAPI::scheduler()
{
    DeliciousAPIJob * nextJob = 0;

    // Do garbage collection and look for jobs waiting to run.
    QPtrListIterator<DeliciousAPIJob> it( m_jobs );
    for ( ; it.current(); ++it ) {
        DeliciousAPIJob* job = it.current();
        if ( job->isDone() )
            m_jobs.remove( job );
        else if ( !job->isRunning() && !nextJob )
            nextJob = job;
    }

    if ( nextJob ) {
        connect( nextJob, SIGNAL( jobStatusText( const QString& ) ), this, SIGNAL( jobStatusText( const QString& ) ) );
        connect( nextJob, SIGNAL( jobProgress( KIO::filesize_t, KIO::filesize_t ) ), this, SIGNAL( jobProgress( KIO::filesize_t, KIO::filesize_t ) ) );
        connect( nextJob, SIGNAL( jobDone() ), this, SLOT( slotJobDone() ) );
        nextJob->start();
    }
}

void DeliciousAPI::slotJobDone()
{
    findUser();
    emit jobDone();
}

RenameTagJob* DeliciousAPI::renameTag( const QString& oldTag, const QString& newTag )
{
    DeliciousRenameTagJob * job = new DeliciousRenameTagJob( this, oldTag, newTag );
    scheduleJob( job );
    return job;
}

GetPostsJob* DeliciousAPI::getPosts( const QDateTime& lastUpdate )
{
    DeliciousGetPostsJob * job = new DeliciousGetPostsJob( this, lastUpdate );
    scheduleJob( job );
    return job;
}



// --- DeliciousRenameTagJob ---

DeliciousRenameTagJob::DeliciousRenameTagJob( DeliciousAPI* api, const QString& oldTag, const QString& newTag )
        : DeliciousAPIJob( api, "RenameTagJob" )
        , m_oldTag( oldTag )
        , m_newTag( newTag )
{}

void DeliciousRenameTagJob::start()
{
    KURL url( apiURL() );
    url.addPath( "tags/rename" );
    url.addQueryItem( "old", m_oldTag );
    url.addQueryItem( "new", m_newTag );
    emit jobStatusText( i18n( "Renaming tag '%1'..." ).arg( m_oldTag ) );
    startJob( url );
}

void DeliciousRenameTagJob::processData()
{
    QDomElement root = m_xml.documentElement();
    ASSERT_XML_ELEMENT( root, "result" );
    if ( root.text() != "done" ) {
        KMessageBox::error( 0, i18n( "Failed to rename the tag \"%1\"." ).arg( m_oldTag ), i18n( "dialog title", "del.icio.us Sidebar" ) );
        emit failed( this );
        return ;
    }
    emit success( this );
}

QString DeliciousRenameTagJob::oldTag() const
{
    return m_oldTag;
}

QString DeliciousRenameTagJob::newTag() const
{
    return m_newTag;
}



// --- DeliciousGetPostsJob ---

DeliciousGetPostsJob::DeliciousGetPostsJob( DeliciousAPI* api, const QDateTime& lastUpdate )
        : DeliciousAPIJob( api, "GetPostsJob" )
        , m_lastUpdate( lastUpdate )
        , m_state( 0 )
        , m_noUpdateNeeded( false )
{}

void DeliciousGetPostsJob::start()
{
    m_state++;
    if ( m_lastUpdate.isNull() )
        m_state++;
    if ( m_state == 1 ) {
        KURL url( apiURL() );
        url.addPath( "posts/update" );
        emit jobStatusText( i18n( "Looking for changes..." ) );
        startJob( url );
    } else if ( m_state == 2 ) {
        KURL url( apiURL() );
        url.addPath( "posts/all" );
        emit jobStatusText( i18n( "Fetching post data..." ) );
        startJob( url );
    }
}

void DeliciousGetPostsJob::processData()
{
    QDomElement root = m_xml.documentElement();
    if ( m_state == 1 ) {
        ASSERT_XML_ELEMENT( root, "update" );
        QDomNamedNodeMap attrs = root.attributes();
        QDomAttr _updateTime = attrs.namedItem( "time" ).toAttr();
        ASSERT_XML_VALID( _updateTime );
        m_updateTime = QDateTime::fromString( _updateTime.value(), ISODate );
        kdDebug() << "Got update time \"" << m_updateTime << "\"" << endl;
        if ( m_updateTime <= m_lastUpdate ) {
            m_noUpdateNeeded = true;
            emit success( this );
            return ;
        }
        start();
    } else if ( m_state == 2 ) {
        ASSERT_XML_ELEMENT( root, "posts" );
        QDomNamedNodeMap attrs = root.attributes();
        QDomAttr postUser = attrs.namedItem( "user" ).toAttr();
        ASSERT_XML_VALID( postUser );
        QDomAttr _updateTime = attrs.namedItem( "update" ).toAttr();
        ASSERT_XML_VALID( _updateTime );
        m_updateTime = QDateTime::fromString( _updateTime.value(), ISODate );
        kdDebug() << "Got update time with posts \"" << m_updateTime << "\"" << endl;
        QDomNodeList posts = root.elementsByTagName( "post" );
        uint i = 0;
        for ( ; i < posts.count(); i++ ) {
            attrs = posts.item( i ).attributes();
            QDomAttr postTitle = attrs.namedItem( "description" ).toAttr();
            ASSERT_XML_VALID( postTitle );
            QDomAttr postDescription = attrs.namedItem( "extended" ).toAttr();
            QDomAttr postURL = attrs.namedItem( "href" ).toAttr();
            ASSERT_XML_VALID( postURL );
            QDomAttr postTags = attrs.namedItem( "tag" ).toAttr();
            QStringList tags;
            if ( !postTags.isNull() )
                tags = QStringList::split( " ", postTags.value() );
            m_posts.append( new Post( postTitle.value(), postDescription.value(), postURL.value(), tags, postUser.value(), this ) );
        }
        emit success( this );
    }
}

const QPtrList<Post>& DeliciousGetPostsJob::posts() const
{
    return m_posts;
}

QDateTime DeliciousGetPostsJob::updateTime() const
{
    return m_updateTime;
}

bool DeliciousGetPostsJob::noUpdateNeeded() const
{
    return m_noUpdateNeeded;
}



#if 0
// --- GetGlobalTagJob ---

GetGlobalTagJob::GetGlobalTagJob( DeliciousAPI* api, const QString& tag )
        : RSSJob( api, "GetGlobalTagJob" )
        , m_tag( tag )
{}

void GetGlobalTagJob::start()
{
    KURL url( "http://del.icio.us/rss/tag" );
    url.addPath( m_tag );
    emit jobStatusText( i18n( "Fetching posts..." ) );
    startJob( url );
}

void GetGlobalTagJob::processFeed( const RSSFeed& feed )
{
    // Post( const QString& title, const QString& description, const KURL& url, const QStringList& tags, const QString& user );
    QValueList<Post> posts;
    QValueList<RSSItem>::ConstIterator i;
    for ( i = feed.items().begin(); i != feed.items().end(); ++i ) {
        QStringList tags = QStringList::split( " ", ( *i ).subject() );
        posts.append( Post( ( *i ).title(), ( *i ).description(), ( *i ).link(), tags, ( *i ).creator() ) );
    }
    kdDebug() << "Post list contains " << posts.count() << " items." << endl;
    emit gotGlobalTag( posts, m_tag );
    emit success();
}
#endif



#include "deliciousapi.moc"
